/* 
 * Copyright (C) 2004-2005 Jonathan Bindel
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Timers;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;
using DCSharp.Settings;

namespace DCSharp.Backend.Managers
{
	public delegate void ConnectionRequestHandler(HubConnection hub, User user);

	public enum RequestStatus
	{
		Unknown,
		Connecting,
		TooManyFailedAttempts,
		UserBusy
	}

	/// <summary>
	/// Handles connection requests to users and is responsible for creating
	/// outgoing and handling incoming user connections. It also maintains a
	/// list of all the available connections.
	/// </summary>
	public class ConnectionManager : IEnumerable<UserConnection>
	{
		public event EventHandler<ConnectionEventArgs> UserConnected;
		public event EventHandler<ConnectionEventArgs> UserDisconnected;
		public event EventHandler<UserEventArgs> UserIdle;

		public const int MaxFailedAttempts = 3;

		private const int TimerInterval = 10000;
		private const int BusyLength = 2 * 60 * 1000;

		private HubManager hubManager;
		private ProtocolHelper helper;
		private IRuntimeSettings settings;

		private List<UserConnection> connections;
		private Dictionary<User, Timer> pending;
		private Dictionary<User, int> failedAttempts;

		private List<User> busyUsers;

		private object listLock; // The lock for the collections above, except "connections"

		public ConnectionManager(HubManager hubManager, ProtocolHelper helper,
			IRuntimeSettings settings)
		{
			if (hubManager == null)
			{
				throw new ArgumentNullException("hubManager");
			}
			if (helper == null)
			{
				throw new ArgumentNullException("helper");
			}
			if (settings == null)
			{
				throw new ArgumentNullException("settings");
			}
			this.hubManager = hubManager;
			this.helper = helper;
			this.settings = settings;

			connections = new List<UserConnection>();
			pending = new Dictionary<User, Timer>();
			failedAttempts = new Dictionary<User, int>();

			busyUsers = new List<User>();

			listLock = new Object();
		}

		#region Properties

		/// <value>
		/// The number of connected users.
		/// </value>
		public int Count
		{
			get { return connections.Count; }
		}

		public object SyncRoot
		{
			get { return connections; }
		}

		#endregion

		#region Methods

		#region Connection

		public UserConnection CreateConnectionTo(IPEndPoint endPoint,
			ConnectionProtocol protocol)
		{
			return Monitor(new UserConnection(endPoint, settings.LocalIdentity,
				protocol, helper));
		}

		public void HandleIncomingConnection(Socket socket)
		{
			UserConnection connection = new UserConnection(socket,
				settings.LocalIdentity, ConnectionProtocol.Unknown, helper);

			Monitor(connection);

			connection.Connect();
		}

		private UserConnection Monitor(UserConnection connection)
		{
			connection.StateChanged += OnStateChanged;
			return connection;
		}

		private void OnStateChanged(object obj, EventArgs args)
		{
			UserConnection connection = (UserConnection)obj;
			if (connection.State == ConnectionState.Connected)
			{
				Debug.Assert(connection.RemoteUser != null);
				lock (connections)
				{
					if (connections.Contains(connection))
					{
						return;
					}
					connections.Add(connection);
					connection.TransferAborted += OnTransferAborted;
				}

				lock (listLock)
				{
					failedAttempts.Remove(connection.RemoteUser);

					Timer timer;
					if (pending.TryGetValue(connection.RemoteUser, out timer))
					{
						timer.Dispose();
						pending.Remove(connection.RemoteUser);
					}
				}
				OnUserConnected(connection);
			}
			else if (connection.State == ConnectionState.Disconnected)
			{
				connection.StateChanged -= OnStateChanged;
				lock (connections)
				{
					if (!connections.Contains(connection))
					{
						return;
					}
					connections.Remove(connection);
					connection.TransferAborted -= OnTransferAborted;
				}
				OnUserDisconnected(connection);
			}
		}

		#endregion

		public bool Contains(UserConnection connection)
		{
			return connections.Contains(connection);
		}

		public IEnumerator<UserConnection> GetEnumerator()
		{
			return connections.GetEnumerator();
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		#region Status

		public bool UserIsBusy(User user)
		{
			return busyUsers.Contains(user);
		}

		public bool IsConnectingTo(User user, out int attempt)
		{
			lock (listLock)
			{
				failedAttempts.TryGetValue(user, out attempt);
				attempt++;

				return pending.ContainsKey(user);
			}
		}

		public RequestStatus GetUserStatus(User user)
		{
			lock (listLock)
			{
				int attempt;
				if (pending.ContainsKey(user))
				{
					return RequestStatus.Connecting;
				}
				else if (busyUsers.Contains(user))
				{
					return RequestStatus.UserBusy;
				}
				else if (failedAttempts.TryGetValue(user, out attempt) &&
					attempt >= MaxFailedAttempts)
				{
					return RequestStatus.TooManyFailedAttempts;
				}
				return RequestStatus.Unknown;
			}
		}

		public void Reset()
		{
			List<User> users = new List<User>();

			// Get all the users to reset
			lock (listLock)
			{
				foreach (User user in failedAttempts.Keys)
				{
					if (!users.Contains(user))
					{
						users.Add(user);
					}
				}
				foreach (User user in busyUsers)
				{
					if (!users.Contains(user))
					{
						users.Add(user);
					}
				}
				failedAttempts.Clear();
				busyUsers.Clear();
			}

			// Emit UserIdle
			foreach (User user in users)
			{
				OnUserIdle(user);
			}
		}

		public void Reset(User user)
		{
			bool emitIdle;
			lock (listLock)
			{
				emitIdle = failedAttempts.ContainsKey(user) ||
					busyUsers.Contains(user);

				failedAttempts.Remove(user);
				busyUsers.Remove(user);
			}
			if (emitIdle)
			{
				OnUserIdle(user);
			}
		}

		#endregion

		#region Request

		public bool RequestConnection(User user,
			ConnectionRequestHandler failedCallback)
		{
			HubConnection hub = hubManager.GetHubWithUser(user);
			if (hub != null)
			{
				return RequestConnection(hub, user, failedCallback);
			}
			return false;
		}

		public bool RequestConnection(HubConnection hub, User user,
			ConnectionRequestHandler failedCallback)
		{
			if (hub == null)
			{
				throw new ArgumentNullException("hub");
			}
			if (user == null)
			{
				throw new ArgumentNullException("user");
			}

			lock (listLock)
			{
				// Check if the requests is allowed
				int attempts;
				if (failedAttempts.TryGetValue(user, out attempts) &&
					attempts >= MaxFailedAttempts)
				{
					return false;
				}
				if (!user.IsOnline || pending.ContainsKey(user) ||
					busyUsers.Contains(user))
				{
					return false;
				}

				// Request if the user is online
				lock (hub.Users.SyncRoot)
				{
					if (hub.Users[user] == null)
					{
						return false;
					}
					hub.RequestConnection(user);
				}

				// Create a timer
				Timer timer = new Timer(TimerInterval);
				timer.Elapsed += delegate
				{
					HandleRequestFailed(user, hub);
					timer.Stop();

					if (failedCallback != null)
					{
						failedCallback(hub, user);
					}
				};
				timer.Start();
				pending.Add(user, timer);
				return true;
			}
		}

		private void HandleRequestFailed(User user, HubConnection hub)
		{
			lock (listLock)
			{
				if (!pending.ContainsKey(user))
				{
					return;
				}
				// Increase the failed attempts
				int attempts = 0;
				failedAttempts.TryGetValue(user, out attempts);

				failedAttempts[user] = attempts + 1;
				pending.Remove(user);
			}
			OnUserIdle(user);
		}

		#endregion

		protected virtual void OnUserConnected(UserConnection connection)
		{
			if (UserConnected != null)
			{
				UserConnected(this, new ConnectionEventArgs(connection));
			}
		}

		protected virtual void OnUserDisconnected(UserConnection connection)
		{
			if (UserDisconnected != null)
			{
				UserDisconnected(this, new ConnectionEventArgs(connection));
			}
		}

		protected virtual void OnUserIdle(User user)
		{
			if (UserIdle != null)
			{
				UserIdle(this, new UserEventArgs(user));
			}
		}

		private void SetBusy(User user)
		{
			lock (listLock)
			{
				if (busyUsers.Contains(user))
				{
					return;
				}
				busyUsers.Add(user);

				Timer timer = new Timer();
				timer.Interval = BusyLength;
				timer.Elapsed += delegate
				{
					timer.Stop();
					lock (listLock)
					{
						busyUsers.Remove(user);
					}
					OnUserIdle(user);
				};
				timer.Start();
			}
		}

		private void OnTransferAborted(object obj, TransferErrorEventArgs args)
		{
			UserConnection connection = (UserConnection)obj;

			if (connection.Direction == TransferDirection.Down &&
				args.Error == TransferError.NoSlots)
			{
				SetBusy(connection.RemoteUser);
				connection.Disconnect();
			}
		}

		#endregion
	}
}
